CrewSettingsTab.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. 'use client';
  2. import { useState } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import { useStudioContext } from '@/app/studio/context';
  5. import type { CrewItem } from '@/types/response/crew/list';
  6. import { Button } from '@/components/ui/button';
  7. import { Input } from '@/components/ui/input';
  8. import { Label } from '@/components/ui/label';
  9. import { Checkbox } from '@/components/ui/checkbox';
  10. function RequiredLabel({ htmlFor, children }: { htmlFor: string; children: React.ReactNode }) {
  11. return <Label htmlFor={htmlFor}><span className="text-destructive mr-0.5">*</span>{children}</Label>;
  12. }
  13. function MoneyInput({ id, value, onChange, placeholder }: { id: string; value: string|number; onChange: (v: string) => void; placeholder?: string }) {
  14. const [focused, setFocused] = useState(false);
  15. const raw = String(value);
  16. const display = focused || !raw ? raw : (Number(raw) ? Number(raw).toLocaleString() : raw);
  17. return (
  18. <Input
  19. id={id} type={focused ? 'number' : 'text'} min={0} placeholder={placeholder}
  20. value={display}
  21. onChange={e => onChange(e.target.value)}
  22. onFocus={() => setFocused(true)}
  23. onBlur={() => setFocused(false)}
  24. />
  25. );
  26. }
  27. type Props = {
  28. crew: CrewItem;
  29. onUpdated: () => void;
  30. };
  31. export default function CrewSettingsTab({ crew, onUpdated }: Props)
  32. {
  33. const { channelID } = useStudioContext();
  34. const [form, setForm] = useState({
  35. name: crew.name,
  36. description: crew.description ?? '',
  37. minAmount: crew.minAmount ?? ('' as string|number),
  38. isActive: crew.isActive
  39. });
  40. const [saving, setSaving] = useState(false);
  41. const handleSave = async () => {
  42. if (!form.name.trim()) {
  43. alert('크루명을 입력해 주세요.'); return;
  44. }
  45. setSaving(true);
  46. try {
  47. await fetchApi('/api/studio/crew/save', {
  48. method: 'POST',
  49. body: {
  50. channelID,
  51. id: crew.id,
  52. name: form.name,
  53. description: form.description || undefined,
  54. minAmount: form.minAmount !== '' ? Number(form.minAmount) : undefined,
  55. isActive: form.isActive
  56. }
  57. });
  58. alert('저장되었습니다.');
  59. onUpdated();
  60. } catch (err: unknown) {
  61. alert(err instanceof Error ? err.message : '저장에 실패했습니다.');
  62. } finally {
  63. setSaving(false);
  64. }
  65. };
  66. return (
  67. <div className="crew-settings">
  68. <div className="space-y-4 max-w-lg">
  69. <div className="space-y-2">
  70. <RequiredLabel htmlFor="crew-name">크루명</RequiredLabel>
  71. <Input id="crew-name" value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} />
  72. </div>
  73. <div className="space-y-2">
  74. <Label htmlFor="crew-desc">설명</Label>
  75. <Input id="crew-desc" value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} />
  76. </div>
  77. <div className="space-y-2">
  78. <Label htmlFor="crew-min">최소 후원금 (원)</Label>
  79. <MoneyInput id="crew-min" placeholder="미설정" value={form.minAmount} onChange={v => setForm(f => ({ ...f, minAmount: v }))} />
  80. <p className="text-xs text-muted-foreground">이 금액 이상 후원한 회원만 크루에 가입할 수 있습니다.</p>
  81. </div>
  82. <div className="flex items-center gap-2">
  83. <Checkbox id="crew-active" checked={form.isActive} onCheckedChange={v => setForm(f => ({ ...f, isActive: !!v }))} />
  84. <Label htmlFor="crew-active">활성화</Label>
  85. </div>
  86. <div className="pt-4 border-t">
  87. <Button onClick={handleSave} disabled={saving}>{saving ? '저장 중...' : '저장'}</Button>
  88. </div>
  89. </div>
  90. </div>
  91. );
  92. }